﻿//****************************************************************************//
//
// @File: Common.cs
// @Owner: lyudmilf
// @Test: 
//
// Purpose:
//
//      Common method, structurs, and constants used in Analyse.cs
//
// Notes:
//      
//      
//
//****************************************************************************//

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml.XPath;

namespace SSAT.ATSDriver
{
    internal static class AnalyseErrors
    {
        private const int AnyState = 10000;
        public const string SysAdminSid = "0x03";
        public const string ServerAccountLockedOutErr = "0x1F";
        public const string ApiErrorsXPathQuery = "/root/row";
        public const string MicrosoftReportDataSeparator = " ";
        public const char UserNameMarker = '\'';
        public const int FormatMessageAllocateBuffer = 256;
        public const int FormatMessageIgnoreInserts = 512;
        public const int FormatMessageFromString = 1024;
        public const int FormatMessageFromHmodule = 2048;
        public const int FormatMessageFromSystem = 4096;
        public const int FormatMessageArgumentArray = 8192;
        public const int FormatMessageMaxWidthMask = 255;
        public const int HexNumber = 518;

        public static int LastError = 0;
        public static int LastStatus = 0;

        [DllImport("kernel32.dll", SetLastError = false)]
        public static extern Int32 FormatMessage(Int32 dwFlags, IntPtr lpSource, Int32 dwMessageId, Int32 dwLanguageId, out StringBuilder lpBuffer, Int32 nSize, IntPtr vaList);

        private delegate void AnalyseDelegate(DataRow errorReader, SqlConnection serverConnection, ReportDetails atsReport);

        private struct ErrorCodesFunc
        {
            private readonly int _error;
            private readonly int _status;
            private readonly AnalyseDelegate _analyseMethod;

            public ErrorCodesFunc(int error, int status, AnalyseDelegate analyseDelegate)
            {
                _error = error;
                _status = status;
                _analyseMethod = analyseDelegate;
            }

            public int ErrorCode 
            { get { return _error; } }

            public int StatusCode 
            { get { return _status; } }

            public AnalyseDelegate AnalyseErrorMethod 
            { get { return _analyseMethod; } }
        }

        private static readonly ErrorCodesFunc[] ErrorCodesMethods = new ErrorCodesFunc[]{
            new ErrorCodesFunc(18456, 1, Analyse.LoginFailedDefaultNoOutput),
            new ErrorCodesFunc(18456, 2, Analyse.LoginFailedSecurityGetLogin1),
            new ErrorCodesFunc(18456, 3, Analyse.LoginFailedSecurityUnprotectMem),
            new ErrorCodesFunc(18456, 4, Analyse.LoginFailedSecurityUnprotectMem),
            new ErrorCodesFunc(18456, 5, Analyse.LoginFailedSecurityGetLogin2),
            new ErrorCodesFunc(18456, 6, Analyse.LoginFailedSecurityLoginType),
            new ErrorCodesFunc(18456, 7, Analyse.LoginFailedSecurityLoginDisabled),
            new ErrorCodesFunc(18456, 8, Analyse.LoginFailedSecurityPasswordNotMatch),
            new ErrorCodesFunc(18456, 9, Analyse.LoginFailedSecurityBadPassword),
            new ErrorCodesFunc(18456, 10, Analyse.LoginFailedSecurityBadResult),
            new ErrorCodesFunc(18456, 11, Analyse.LoginFailedSecurityFastCheckServerAccess1),
            new ErrorCodesFunc(18456, 12, Analyse.LoginFailedSecurityFastCheckServerAccess2),
            new ErrorCodesFunc(18456, 13, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 14, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 15, Analyse.LoginFailedLoginSwitchDb),
            new ErrorCodesFunc(18456, 16, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 17, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 18, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 19, Analyse.LoginFailedSecurityUnprotectMem),
            new ErrorCodesFunc(18456, 20, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 21, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 22, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 23, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 24, Analyse.LoginFailedLoginSwitchDb),
            new ErrorCodesFunc(18456, 25, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 26, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 27, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 28, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 29, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 30, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 31, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 32, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 33, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 34, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 35, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 36, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 37, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 38, Analyse.LoginFailedLoginSessionDbUseDbExplicit),
            new ErrorCodesFunc(18456, 39, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 40, Analyse.LoginFailedLoginSessionDbUseDbImplicit),
            new ErrorCodesFunc(18456, 41, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 42, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 43, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 44, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 45, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 46, Analyse.LoginFailedLoginSessionDbUseDbExplicit),
            new ErrorCodesFunc(18456, 47, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 48, Analyse.LoginFailedLoginSessionDbUseDbImplicit),
            new ErrorCodesFunc(18456, 49, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 50, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 51, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 52, Analyse.LoginFailedSecurityImpersonationOrToken),
            new ErrorCodesFunc(18456, 53, Analyse.LoginFailedSecurityImpersonationOrToken),
            new ErrorCodesFunc(18456, 54, Analyse.LoginFailedSecurityImpersonationOrToken),
            new ErrorCodesFunc(18456, 55, Analyse.LoginFailedSecurityImpersonationOrToken),
            new ErrorCodesFunc(18456, 56, Analyse.LoginFailedSecurityImpersonationOrToken),
            new ErrorCodesFunc(18456, 57, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18456, 58, Analyse.LoginFailedWindowsAuthentificatinOnlyError),
            new ErrorCodesFunc(18451, AnyState, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18452, AnyState, Analyse.LoginFailedLoginInvalidConnect),
            new ErrorCodesFunc(18458, AnyState, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18459, AnyState, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18460, AnyState, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18461, AnyState, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18462, AnyState, Analyse.LoginFailedLogonPasswordChangeFailed),
            new ErrorCodesFunc(18463, AnyState, Analyse.LoginFailedLogonPasswordChangeFailed),
            new ErrorCodesFunc(18464, AnyState, Analyse.LoginFailedLogonPasswordChangeFailed),
            new ErrorCodesFunc(18465, AnyState, Analyse.LoginFailedLogonPasswordChangeFailed),
            new ErrorCodesFunc(18466, AnyState, Analyse.LoginFailedLogonPasswordChangeFailed),
            new ErrorCodesFunc(18467, AnyState, Analyse.LoginFailedLogonPasswordChangeFailed),
            new ErrorCodesFunc(18468, AnyState, Analyse.LoginFailedLogonPasswordChangeFailed),
            new ErrorCodesFunc(18470, AnyState, Analyse.LoginFailedLoginDisabled),
            new ErrorCodesFunc(18471, AnyState, Analyse.LoginFailedDefault),
            new ErrorCodesFunc(18486, AnyState, Analyse.LoginFailedLogonAccountLockedOut),
            new ErrorCodesFunc(18487, AnyState, Analyse.LoginFailedLogonPasswordExpired),
            new ErrorCodesFunc(18488, AnyState, Analyse.LoginFailedLogonMustChangePassword)
        };

        public static void FindErrorAndAnalyse(DataRow errorReader, SqlConnection serverConnection, ReportDetails atsReport)
        {
            int errorCode = (int)errorReader[Constants.AtsDriver.ErrorNumberColumn];
            int statusCode = (int)errorReader[Constants.AtsDriver.ErrorStateColumn];

            foreach (ErrorCodesFunc errorCodesFunc in ErrorCodesMethods)
            {
                if (errorCodesFunc.ErrorCode != errorCode || (errorCodesFunc.StatusCode != statusCode && errorCodesFunc.StatusCode != AnyState))
                {
                    continue;
                }
                errorCodesFunc.AnalyseErrorMethod(errorReader, serverConnection, atsReport);
                atsReport.Write(ATSDriverResources.SeparatingLine);
                return;
            }
            atsReport.Write(ATSDriverResources.UnknownError);
            atsReport.Write(errorReader[Constants.AtsDriver.ErrorMessageColumn].ToString());
            return;
        }

        public static string GetErrorMessage(string errorCode)
        {
            StringBuilder errorMessage;
            try
            {
                if (errorCode.Contains(Constants.System.HexNumberIndicator))
                {
                    errorCode = errorCode.Replace(Constants.System.HexNumberIndicator, string.Empty);
                }
                const int flags = FormatMessageAllocateBuffer | FormatMessageFromSystem | FormatMessageIgnoreInserts;
                int nErrorCode = Int32.Parse(errorCode, NumberStyles.HexNumber);

                FormatMessage(flags, IntPtr.Zero, nErrorCode, 0, out errorMessage, 0, IntPtr.Zero);
            }
            catch
            {
                return ATSDriverResources.UnknownError;
            }
            if (errorMessage.Length == 0)
            {
                errorMessage.Append(ATSDriverResources.UnknownError);
            }

            return errorMessage.ToString();
        }

        private static string GetUserNameSurroundedByUserNameMarkers(string text)
        {
            return string.Format("{0}{1}{0}", UserNameMarker, text);
        }

        private static string GetEmptyUserName()
        {
            return GetUserNameSurroundedByUserNameMarkers(string.Empty);
        }

        // Should always return a string with quotation marks (Ex.:'user_01', 'Domain\User', '')
        //
        public static string GetUserName(DataRow errorReader)
        {
            string errorString = errorReader[Constants.AtsDriver.ErrorMessageColumn].ToString();
            string userName = errorReader[Constants.AtsDriver.UserNameColumn].ToString();
            string ntUserName = errorReader[Constants.AtsDriver.SessionNtUserName].ToString();
            if (!userName.Equals(string.Empty))
            {
                return GetUserNameSurroundedByUserNameMarkers(userName);
            }
            if (!ntUserName.Equals(string.Empty))
            {
                return GetUserNameSurroundedByUserNameMarkers(ntUserName);
            }
            int idx1 = errorString.IndexOf(UserNameMarker);
            int idx2 = errorString.LastIndexOf(UserNameMarker);
            string userNameProvided;
            if (idx1 < 0 || idx2 < 0 || idx2 <= idx1)
            {
                userNameProvided = GetEmptyUserName();
            }
            else
            {
                userNameProvided = errorString.Substring(idx1, idx2 - idx1 + 1);
            }
            if (userNameProvided[0].Equals(UserNameMarker) && userNameProvided[userNameProvided.Length - 1].Equals(UserNameMarker) && userNameProvided.Length > 2)
            {
                return userNameProvided;
            }
            return GetEmptyUserName();
        }

        public static XPathNodeIterator GetApiErrors(string rbXml)
        {
            XPathNodeIterator errors;
            try
            {
                Stream sRbXml = new MemoryStream(Encoding.Default.GetBytes(rbXml));
                var documentErrors = new XPathDocument(sRbXml);
                XPathNavigator errorsNavigator = documentErrors.CreateNavigator();
                errors = errorsNavigator.Select(ApiErrorsXPathQuery);
            }
            catch
            {
                return null;
            }
            return errors;
        }

        public static int GetLoginTokenMembership(SqlConnection serverConnection, LinkedList<string> loginTokens, string sqlCommandTest, bool sid)
        {
            SqlDataReader serverReader = null;
            try
            {
                SqlCommand serverCommand = new SqlCommand(sqlCommandTest, serverConnection);
                serverReader = serverCommand.ExecuteReader();
                while (serverReader.Read())
                {
                    string group = serverReader.GetValue(0).ToString();
                    loginTokens.AddLast(sid ? group : GetUserNameSurroundedByUserNameMarkers(group));
                }
            }
            catch
            {
                return -1;
            }
            finally
            {
                if (serverReader != null)
                {
                    serverReader.Close();
                }
            }
            return 0;
        }

        private static readonly string[] WinApis = new string[]{
            Constants.AtsDriver.WinApi.NetValidatePasswordPolicy,
            "NetUserModalsGet",
            "OpenProcess",
            "OpenProcessToken",
            "OpenThreadToken",
            "GetTokenInformation",
            "DuplicateTokenEx",
            "GetSecurityInfo",
            "SetSecurityInfo",
            "LookupPrivilegeValue",
            "AdjustTokenPrivileges",
            "LoadUserProfile",
            "ImpersonateLoggedOnUser",
            "ImpersonateSelf",
            Constants.AtsDriver.WinApi.ImpersonateSecurityContext,
            "SetEntriesInAcl",
            "AllocateLocallyUniqueId",
            "LookupAccountName",
            "LookupAccountSid",
            "ConvertStringSidToSid",
            "LogonUserEx",
            "LogonUser",
            "LsaConnectUntrusted",
            "LsaLookupAuthenticationPackage",
            "LsaLogonUser",
            "DsGetDcName",
            "CreateProcessAsUser",
            "GetProcessTimes",
            "OpenEvent",
            "SetEvent",
            "GetLongPathName",
            "CreateDirectory",
            "GetFileAttributesEx",
            "CopyFile",
            "WriteFile",
            "ReadFile",
            "SetFilePointer",
            "RegCreateKeyEx",
            "RegSetValueEx",
            "RegOpenKeyEx",
            "RegQueryValueEx",
            "SHGetFolderPath",
            "MultiByteToWideChar",
            "StringCchPrintf",
            "StringCchCat",
            "CoCreateInstance",
            "ICommandText::SetCommandText",
            "ICommandText::Execute",
            "IDataInitialize::GetDataSource",
            "IDbInitialize::Initialize",
            "IDbInitialize::QueryInterface",
            "IDbCreateSession::CreateSession",
            "IDbCreateCommand::CreateCommand",
            "CryptAcquireContext",
            "CryptGenRandom"
        };

        public static bool IsWinApi(string functionName)
        {
            if (!string.IsNullOrEmpty(functionName))
            {
                foreach (string winApi in WinApis)
                {
                    if (winApi.Equals(functionName, StringComparison.OrdinalIgnoreCase))
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        public static ReportDetails SendMsReport(DataTable resultTable, ReportDetails atsReport)
        {
            ReportDetails msReport = new ReportDetails(atsReport);
            foreach (DataRow dataRow in resultTable.Rows)
            {
                string[] values = new string[] 
                {
                    dataRow[Constants.AtsDriver.ErrorNumberColumn].ToString(),
                    dataRow[Constants.AtsDriver.ErrorStateColumn].ToString(),
                    dataRow[Constants.AtsDriver.ErrorMessageColumn].ToString(),
                    dataRow[Constants.AtsDriver.DataBaseIdColumn].ToString(),
                    dataRow[Constants.AtsDriver.SqlTextColumn].ToString(),
                    dataRow[Constants.AtsDriver.DataBaseContextColumn].ToString(),
                    dataRow[Constants.AtsDriver.ClientApplicationNameColumn].ToString(),
                    dataRow[Constants.AtsDriver.ClientPid].ToString(),
                    dataRow[Constants.AtsDriver.ClientHostName].ToString(),
                    dataRow[Constants.AtsDriver.NtUserNameColumn].ToString(),
                    dataRow[Constants.AtsDriver.SessionNtUserName].ToString(),
                    dataRow[Constants.AtsDriver.UserNameColumn].ToString(),
                    dataRow[Constants.AtsDriver.SessionIdColumn].ToString(),
                    dataRow[Constants.AtsDriver.RbErrorColumn].ToString(),
                    dataRow[Constants.AtsDriver.DataBaseNameColumn].ToString(),
                };
                string row = string.Join(MicrosoftReportDataSeparator, values);
                msReport.Write(row);
            }
            return msReport;
        }

        public static void CheckUserMapping(SqlConnection serverConnection, ReportDetails atsReport, string userNameProvided, string databaseName)
        {
            LinkedList<string> loginTokens = new LinkedList<string>();
            GetLoginTokenMembership(serverConnection, loginTokens, string.Format(Queries.GetLoginTokenSid, userNameProvided), true);

            SqlDataReader serverReader = null;
            //if sysadmin sckip following checks
            if (loginTokens.Find(SysAdminSid) == null)
            {
                SqlCommand serverCommand = new SqlCommand(string.Format(Queries.Users, databaseName), serverConnection);
                serverReader = serverCommand.ExecuteReader();
                bool userExists = false;
                string guestPermission = Constants.AtsDriver.Permissions.Revoked;
                while (serverReader.Read())
                {
                    if ((loginTokens.Find(serverReader.GetValue(0).ToString()) == null &&
                         !serverReader.GetValue(1).ToString().Equals(Constants.AtsDriver.LoginTokens.Public)) &&
                        !serverReader.GetValue(1).ToString().Equals(Constants.AtsDriver.LoginTokens.Guest))
                    {
                        continue;
                    }
                    if (!serverReader.IsDBNull(0) && serverReader.GetValue(4).ToString().Equals(Constants.AtsDriver.Permissions.Denied))
                    {
                        if (serverReader.GetValue(1).ToString().Equals(Constants.AtsDriver.LoginTokens.Guest))
                        {
                            guestPermission = Constants.AtsDriver.Permissions.Denied;
                        }
                        else
                        {
                            atsReport.WriteFormat(ATSDriverResources.ConnectPermissionDenied, serverReader.GetValue(1).ToString());
                            atsReport.WriteFormat(ATSDriverResources.DeniedBy, serverReader.GetValue(3).ToString());
                            userExists = true;
                        }
                    }
                    if (serverReader.IsDBNull(0) ||
                        !serverReader.GetValue(4).ToString().Equals(Constants.AtsDriver.Permissions.Granted))
                    {
                        continue;
                    }
                    if (serverReader.GetValue(1).ToString().Equals(Constants.AtsDriver.LoginTokens.Guest))
                    {
                        guestPermission = Constants.AtsDriver.Permissions.Granted;
                    }
                    else
                    {
                        userExists = true;
                    }
                }
                if (!userExists && guestPermission != Constants.AtsDriver.Permissions.Granted)
                {
                    atsReport.WriteFormat(ATSDriverResources.LoginNotMapped, userNameProvided);
                    atsReport.WriteFormat(ATSDriverResources.GuestDoesNotHavePermissionsToConnect, userNameProvided);
                }
                serverReader.Close();
            }
            return;
        }

        public static void CheckApiFailures(DataRow errorReader, ReportDetails atsReport, bool fSqlAuth)
        {
            XPathNodeIterator iApiErrors = GetApiErrors(errorReader[Constants.AtsDriver.RbErrorColumn].ToString());
            if (iApiErrors != null)
            {
                string errorCode;
                foreach (XPathNavigator nodeApiErrors in iApiErrors)
                {
                    if (!nodeApiErrors.GetAttribute(Constants.AtsDriver.ApiFailuresAttributes.SPID, string.Empty).Equals(errorReader[Constants.AtsDriver.SessionIdColumn].ToString()) &&
                        IsWinApi(nodeApiErrors.GetAttribute(Constants.AtsDriver.ApiFailuresAttributes.ApiName, string.Empty)))
                    {
                        continue;
                    }
                    // If this is SQL authentication, "ImpersonateSecurityContext" will always fail - this is expected.
                    if (fSqlAuth &&
                        nodeApiErrors.GetAttribute(Constants.AtsDriver.ApiFailuresAttributes.ApiName, string.Empty).Equals(Constants.AtsDriver.WinApi.ImpersonateSecurityContext, StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }

                    errorCode = nodeApiErrors.GetAttribute(Constants.AtsDriver.ApiFailuresAttributes.ErrorCode, string.Empty);
                    atsReport.WriteFormat(ATSDriverResources.WinApiFailed, nodeApiErrors.GetAttribute(Constants.AtsDriver.ApiFailuresAttributes.ApiName, string.Empty));
                    string winErrorMessage = GetErrorMessage(errorCode);
                    atsReport.WriteFormat(ATSDriverResources.WithError, errorCode);
                    atsReport.Write(winErrorMessage);
                    atsReport.Write(ATSDriverResources.CheckMSDNToSeeWhatCouldCauseThisError);

                    if (!errorCode.Equals(ServerAccountLockedOutErr, StringComparison.OrdinalIgnoreCase) ||
                        !(nodeApiErrors.GetAttribute(Constants.AtsDriver.ApiFailuresAttributes.ApiName, string.Empty)).
                             Equals(Constants.AtsDriver.WinApi.NetValidatePasswordPolicy,
                                    StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }
                    atsReport.Write(ATSDriverResources.ServiceAccountLockedOut);
                    atsReport.Write(ATSDriverResources.TurnOnTraceFlag4614ToAllowAuthentificationSucceeed);
                    return;
                }
            }
            return;
        }
    }
}